/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.util.core;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.util.CommonConverter;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.common.util.PartialCollection;
import com.blandware.atleap.common.util.StringUtil;
import com.blandware.atleap.model.core.*;
import com.blandware.atleap.service.core.ContentResourceManager;
import com.blandware.atleap.service.core.LayoutManager;
import com.blandware.atleap.service.core.LookupManager;
import com.blandware.atleap.service.core.PageManager;
import com.blandware.atleap.webapp.struts.HeritableComponentDefinition;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.validator.GenericValidator;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.config.ForwardConfig;
import org.apache.struts.config.ModuleConfig;
import org.apache.struts.tiles.DefinitionsFactoryException;
import org.apache.struts.tiles.TilesUtil;
import org.apache.struts.util.MessageResources;
import org.apache.struts.util.ModuleUtils;
import org.apache.struts.util.RequestUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.*;

/**
 * <p>Utils to use in web module</p>
 * <p><a href="WebappUtil.java.html"><i>View Source</i></a></p>
 *
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.63 $ $Date: 2008/01/13 15:27:07 $
 */
public class WebappUtil {

	/**
	 * Gets index of indexed ContentField (it's extracted from identifier)
	 *
	 * @param identifier e.g. a[0.5]
	 * @return index or <code>null</code> if no index found or it cannot be
	 *         converted to Double
	 */
	public static String getContentFieldIndex(String identifier) {
		if ( identifier == null ) {
			return null;
		}

		String result = null;
		int openBracket = identifier.lastIndexOf("[");
		int closeBracket = identifier.lastIndexOf("]");
		if ( openBracket > 0 && closeBracket > 2 && closeBracket - openBracket > 1 ) {
			try {
				// this is done just to check whether index value is valid number
				Double.valueOf(identifier.substring(openBracket + 1, closeBracket));
				result = identifier.substring(openBracket + 1, closeBracket);
			} catch ( Exception ex ) {
				//do nothing
			}
		}
		return result;
	}

	/**
	 * Joins two collections: parent field collection and child field collection.
	 * If some field (from parent field collection) with the same identifier
	 * exists in child field collection, it will be overriden.
	 *
	 * @param parentFields collection of fields with lower precedence
	 * @param childFields  collection of fields with higher precedense
	 * @return joined collection
	 */
	public static Collection joinFields(Collection parentFields, Collection childFields) {
		if ( parentFields == null || parentFields.size() == 0 ) {
			return childFields;
		}
		if ( childFields == null || childFields.size() == 0 ) {
			return parentFields;
		}
		Map fields = new HashMap();
		for ( Iterator iterator = parentFields.iterator(); iterator.hasNext(); ) {
			ContentField contentField = (ContentField) iterator.next();
			String key = contentField.getIdentifier();
			fields.put(key, contentField);
		}
		for ( Iterator iterator = childFields.iterator(); iterator.hasNext(); ) {
			ContentField contentField = (ContentField) iterator.next();
			String key = contentField.getIdentifier();
			fields.put(key, contentField);
		}
		return fields.values();
	}

	/**
	 * Converts collection of <code>Role</code> objects to comma-separated list
	 * of their names (identifiers)
	 *
	 * @param roles Collection of roles
	 * @return Comma-separated list of roles
	 */
	public static String rolesToString(Collection roles) {
		if ( roles == null || roles.size() == 0 ) {
			return "";
		}
		List roleNames = new ArrayList();
		//getting names of roles
		for ( Iterator iterator = roles.iterator(); iterator.hasNext(); ) {
			Role role = (Role) iterator.next();
			roleNames.add(role.getName());
		}
		return ConvertUtil.convertListToString(roleNames, ",");
	}

	/**
	 * Returns the form action converted into a server-relative URL adding a
	 * default locale suffix to it.
	 *
	 * @param action  can be action name or full action with extension and query string
	 * @param module  struts module name, if <code>null</code> the current will be used
	 * @param request Servlet request we are proceeding
	 * @param urlType Value to mark URL must be domain, context or module relative
	 *                If it is equal to <code>URL_DOMAIN_RELATIVE</code> resulting value will include context path and module prefix
	 *                Else if it is equal to <code>URL_CONTEXT_RELATIVE</code> resulting value will include module prefix only
	 *                Else resulting value will include only URL itself without any prefixes.
	 * @return URL of requested action mapping
	 */
	public static String getActionMappingURL(String action, String module, HttpServletRequest request, int urlType) {
		return getActionMappingURL(action, module, request, urlType, true);
	}

	/**
	 * Returns the form action converted into a server-relative URL.
	 *
	 * @param action          can be action name or full action with extension and query string
	 * @param module          struts module name, if <code>null</code> the current will be used
	 * @param request         Servlet request we are proceeding
	 * @param urlType         Value to mark URL must be domain, context or module relative
	 *                        If it is equal to <code>URL_DOMAIN_RELATIVE</code> resulting value will include context path and module prefix
	 *                        Else if it is equal to <code>URL_CONTEXT_RELATIVE</code> resulting value will include module prefix only
	 *                        Else resulting value will include only URL itself without any prefixes.
	 * @param addLocaleSuffix Whether or not to add default locale suffix
	 * @return URL of requested action mapping
	 */
	public static String getActionMappingURL(String action, String module, HttpServletRequest request, int urlType, boolean addLocaleSuffix) {
		Locale locale = WebappUtil.getEnsuredUserLocale(request);
		String language = null;
		if ( locale != null && addLocaleSuffix ) {
			language = locale.getLanguage();
		}
		return getActionMappingURL(action, module, request, urlType, language);
	}


	/**
	 * Returns the form action converted into a server-relative URL.
	 *
	 * @param action       can be action name or full action with extension and query string
	 * @param module       struts module name, if <code>null</code> the current will be used
	 * @param request      Servlet request we are proceeding
	 * @param urlType      Value to mark URL must be domain, context or module relative
	 *                     If it is equal to <code>URL_DOMAIN_RELATIVE</code> resulting value will include context path and module prefix
	 *                     Else if it is equal to <code>URL_CONTEXT_RELATIVE</code> resulting value will include module prefix only
	 *                     Else resulting value will include only URL itself without any prefixes.
	 * @param localeSuffix Locale suffix to add
	 * @return URL of requested action mapping
	 */
	public static String getActionMappingURL(String action, String module, HttpServletRequest request, int urlType, String localeSuffix) {

		StringBuffer url = new StringBuffer();
		ServletContext servletContext = request.getSession().getServletContext();
		ModuleConfig moduleConfig = ModuleUtils.getInstance().getModuleConfig(module, request, servletContext);

		// Use our servlet mapping, if one is specified
		String servletMapping = (String) servletContext.getAttribute(Globals.SERVLET_KEY);

		// append context path if requested
		if ( urlType == WebappConstants.URL_TYPE_DOMAIN_RELATIVE ) {
			url.append(request.getContextPath());
		}

		// append an /rw prefix
		if ( servletMapping != null && servletMapping.startsWith("*.") ) {
			if ( localeSuffix != null && localeSuffix.trim().length() > 0 && !action.startsWith(Constants.LOCALIZED_URI_PREFIX) ) {
				url.append(Constants.LOCALIZED_URI_PREFIX);
			}
		}

		// append module prefix if requested
		if ( (urlType == WebappConstants.URL_TYPE_DOMAIN_RELATIVE || urlType == WebappConstants.URL_TYPE_CONTEXT_RELATIVE) && moduleConfig != null ) {
			url.append(moduleConfig.getPrefix());
		}

		// construct an URL for specified action
		if ( servletMapping != null ) {

			String queryString = null;
			int questionMark = action.indexOf("?");
			if ( questionMark >= 0 ) {
				queryString = action.substring(questionMark);
			}

			String actionMapping = getActionMappingName(action);
			if ( servletMapping.startsWith("*.") ) {

				url.append(actionMapping);
				if ( localeSuffix != null && localeSuffix.trim().length() > 0 ) {
					url.append(".").append(localeSuffix);
				}
				url.append(servletMapping.substring(1));
			} else if ( servletMapping.endsWith("/*") ) {
				url.append(servletMapping.substring(0, servletMapping.length() - 2));
				url.append(actionMapping);

			} else if ( servletMapping.equals("/") ) {
				url.append(actionMapping);
			}

			if ( queryString != null ) {
				url.append(queryString);
			}
		}

		// Otherwise, assume extension mapping is in use and extension is
		// already included in the action property
		else {
			if ( !action.startsWith("/") ) {
				url.append("/");
			}
			url.append(action);
		}

		return url.toString();
	}

	/**
	 * Returns the form action converted into an action mapping path.  The
	 * value of the <code>action</code> property is manipulated as follows in
	 * computing the name of the requested mapping:
	 * <ul>
	 * <li>Any filename extension is removed (in the theory that extension
	 * mapping is being used to select the controller servlet).</li>
	 * <li>If the resulting value does not start with a slash, then a
	 * slash is prepended.</li>
	 * </ul>
     *
     * @param action action URI
     * @return mapping name
	 */
	protected static String getActionMappingName(String action) {

		String value = action;
		int question = action.indexOf("?");
		if ( question >= 0 ) {
			value = value.substring(0, question);
		}

		int slash = value.lastIndexOf("/");
		int period = value.lastIndexOf(".");
		if ( (period >= 0) && (period > slash) ) {
			value = value.substring(0, period);
		}

		return value.startsWith("/") ? value : ("/" + value);
	}

	/**
	 * Searches for configuration object for specified Global Action Forward
	 *
	 * @param globalForward Name of Global Forward to find config for
	 * @param module        Name of module or <code>null</code> if current is used
	 * @param request       HTTP Servlet request we're processing
	 * @return Configuration object for specified Global Forward or <code>null</code>
	 */
	public static ForwardConfig findGlobalForwardConfig(String globalForward, String module, HttpServletRequest request) {
		ServletContext servletContext = request.getSession().getServletContext();
		ModuleConfig moduleConfig = ModuleUtils.getInstance().getModuleConfig(module, request, servletContext);
		ForwardConfig forwardConfig = null;
		if ( moduleConfig != null ) {
			forwardConfig = moduleConfig.findForwardConfig(globalForward);
		}
		return forwardConfig;
	}


	/**
	 * Returns the URL corresponding to specified Global Action Forward adding
	 * a default locale suffix
	 *
	 * @param globalForward Global forward name
	 * @param module        struts module name. If <code>null</code>, the current will be used
	 * @param request       Servlet request we are proceeding
     * @param urlType       URL type
	 * @return URL corresponding to the specified Global Action Forward
     * @throws java.net.MalformedURLException if no such global forvard exists
	 */
	public static String getActionForwardURL(String globalForward, String module, HttpServletRequest request, int urlType) throws MalformedURLException {
		return getActionForwardURL(globalForward, module, request, urlType, true);
	}

	/**
	 * Returns the URL corresponding to specified Global Action Forward
	 *
	 * @param globalForward   Global forward name
	 * @param module          struts module name. If <code>null</code>, the current will be used
	 * @param request         Servlet request we are proceeding
     * @param urlType         URL type
	 * @param addLocaleSuffix Whether or not to add default locale suffix
	 * @return URL corresponding to the specified Global Action Forward
     * @throws java.net.MalformedURLException if no such global forvard exists
	 */
	public static String getActionForwardURL(String globalForward, String module, HttpServletRequest request, int urlType, boolean addLocaleSuffix) throws MalformedURLException {
		Locale locale = WebappUtil.getEnsuredUserLocale(request);
		String language = null;
		if ( locale != null && addLocaleSuffix ) {
			language = locale.getLanguage();
		}
		return getActionForwardURL(globalForward, module, request, urlType, language);
	}

	/**
	 * Returns the URL corresponding to specified Global Action Forward
	 *
	 * @param globalForward Global forward name
	 * @param module        Struts module name. If <code>null</code>, current will be used
	 * @param request       Servlet request we are proceeding
     * @param urlType       URL type
	 * @param localeSuffix  Locale suffix to add
	 * @return URL corresponding to the specified Global Action Forward
     * @throws java.net.MalformedURLException if no such global forvard exists
	 */
	public static String getActionForwardURL(String globalForward, String module, HttpServletRequest request, int urlType, String localeSuffix) throws MalformedURLException {
		ForwardConfig forwardConfig = findGlobalForwardConfig(globalForward, module, request);
		if ( forwardConfig == null ) {
			throw new MalformedURLException("No global forward with name '" + globalForward + "' could be found");
		}
		return getActionForwardURL(forwardConfig, module, request, urlType, localeSuffix);
	}

	/**
	 * Returns the URL corresponding to specified ActionForward adding a default
	 * locale suffix
	 *
	 * @param forwardConfig ForwardConfig instance, representing logical ActionForward to get URL for
	 * @param module        struts module name. If <code>null</code>, the current will be used
	 * @param request       Servlet request we are proceeding
     * @param urlType       URL type
	 * @return URL corresponding to the specified Global ActionForward
	 */
	public static String getActionForwardURL(ForwardConfig forwardConfig, String module, HttpServletRequest request, int urlType) {
		return getActionForwardURL(forwardConfig, module, request, urlType, true);
	}

	/**
	 * Returns the URL corresponding to specified ActionForward
	 *
	 * @param forwardConfig   ForwardConfig instance, representing logical ActionForward to get URL for
	 * @param module          struts module name. If <code>null</code>, the current will be used
	 * @param request         Servlet request we are proceeding
     * @param urlType         URL type
	 * @param addLocaleSuffix Whether or not to add default locale suffix
	 * @return URL corresponding to the specified Global Action Forward
	 */
	public static String getActionForwardURL(ForwardConfig forwardConfig, String module, HttpServletRequest request, int urlType, boolean addLocaleSuffix) {
		Locale locale = WebappUtil.getEnsuredUserLocale(request);
		String language = null;
		if ( locale != null && addLocaleSuffix ) {
			language = locale.getLanguage();
		}
		return getActionForwardURL(forwardConfig, module, request, urlType, language);
	}

	/**
	 * Return the URL corresponding to specified ActionForward
	 *
	 * @param forwardConfig ForwardConfig instance, representing logical ActionForward to get URL for
	 * @param module        struts module name. If <code>null</code>, the current will be used
	 * @param request       Servlet request we are proceeding
     * @param urlType       URL type
	 * @param localeSuffix  Locale suffix to add (can be null)
	 * @return URL corresponding to the specified Global Action Forward
	 */
	public static String getActionForwardURL(ForwardConfig forwardConfig, String module, HttpServletRequest request, int urlType, String localeSuffix) {
		StringBuffer url = new StringBuffer();
		ServletContext servletContext = request.getSession().getServletContext();
		ModuleConfig moduleConfig = ModuleUtils.getInstance().getModuleConfig(module, request, servletContext);
		String path = forwardConfig.getPath();

		if ( !isExternalURL(path, request) ) {

			if ( !isAbsoluteURL(path) ) {
				if ( urlType == WebappConstants.URL_TYPE_DOMAIN_RELATIVE ) {
					String contextPath = request.getContextPath();
					if ( !path.startsWith(contextPath) && !contextPath.equals("/") ) {
						url.append(contextPath);
					}
				}
				url.append(RequestUtils.forwardURL(request, forwardConfig, moduleConfig));
			} else {
				url.append(path);
			}

			// add locale suffix if requested
			if ( localeSuffix != null && localeSuffix.trim().length() > 0 ) {
				// Use our servlet mapping, if one is specified
				String servletMapping = (String) servletContext.getAttribute(Globals.SERVLET_KEY);
				String ext;
				if ( servletMapping != null && servletMapping.startsWith("*.") ) {
					ext = servletMapping.substring(1);
				} else {
					ext = ".jsp";
				}

				String temp = url.toString();
				int questionMark = url.indexOf("?");
				if ( questionMark >= 0 ) {
					temp = url.substring(0, questionMark);
				}

				// dot before extension
				int extDot = temp.lastIndexOf(ext);
				if ( extDot < 0 && !ext.equals(".jsp") ) {
					extDot = temp.lastIndexOf(".jsp");
				}
				if ( extDot > 0 ) {

					// first insert an /rw prefix

					int fromIndex = 0;
					if ( isAbsoluteURL(path) ) {
						// look up scheme
						fromIndex = path.indexOf("://");
						if ( fromIndex < 0 ) {
							fromIndex = path.indexOf(":") + 1;
						} else {
							fromIndex += 3;
						}
					}

					String contextPath = request.getContextPath();
					if ( contextPath == null || contextPath.length() == 0 ) {
						contextPath = "/";
					}

					// save suffix pos for later use
					int suffixPos = extDot;

                    // length of text inserted in url to use correct positions
                    int correction = 0;

					// insert prefix if it does not exist
					int insertPos;
                    if ("/".equals(contextPath)) {
                        insertPos = url.indexOf(contextPath, fromIndex);
                    } else {
                        insertPos = url.indexOf(contextPath, fromIndex) + contextPath.length();
                    }

                    if ( url.indexOf(Constants.LOCALIZED_URI_PREFIX, insertPos) != insertPos ) {
						url.insert(insertPos, Constants.LOCALIZED_URI_PREFIX);
                        correction = Constants.LOCALIZED_URI_PREFIX.length();
						suffixPos += correction;
					}

					temp = temp.substring(0, extDot);

					// dot before language
					int langDot = temp.lastIndexOf(".");
					if ( langDot != -1 && langDot + correction == suffixPos - 3 ) {
						url.replace(langDot + correction, suffixPos, "." + localeSuffix);
					} else {
						url.insert(suffixPos, "." + localeSuffix);
					}
				}
			}

		} else {
			url.append(path);
		}

		return url.toString();
	}

	/**
	 * Computes URL from given values. Either <code>action</code> or <code>forward</code> or <code>href</code> must be non-null.
	 * Encodes all non-ASCII characters (if <code>encode</code> is <code>true</code>) and adds <code>jsessionid</code> to the generated link.
	 * If URL is absolute, it will not be encoded. Default locale suffix is added
	 * to the result.
	 *
	 * @param action     Name of Action
	 * @param forward    Name of Global Action Forward
	 * @param href       Link which will be added to result without changes to its body
	 * @param anchor     Anchor to append to the end of URL
	 * @param parameters Parameters to add to generated link
	 * @param request    HTTP Servlet request we're currently proceeding
	 * @param response   HTTP Servlet response we're creating
	 * @param encode     Whether or not to encode non-ASCII characters
	 * @param redirect   Whether or not we're generating an URL to use in <code>javax.servlet.http.HttpServletResponse#sendRedirect(String)</code> method
	 * @return Generated URL
	 * @throws MalformedURLException if URL is malformed, e.g. Global Action Forward with specified name could not be found
	 * @see javax.servlet.http.HttpServletResponse#sendRedirect(String)
	 */
	public static String computeURL(String action, String forward, String href, String anchor, Map parameters, HttpServletRequest request, HttpServletResponse response, boolean encode, boolean redirect) throws MalformedURLException {
		return computeURL(action, forward, href, anchor, parameters, request, response, encode, redirect, true);
	}

	/**
	 * Computes URL from given values. Either <code>action</code> or <code>forward</code> or <code>href</code> must be non-null.
	 * Encodes all non-ASCII characters (if <code>encode</code> is true) and adds <code>jsessionid</code> to the generated link.
	 * If URL is absolute, it will not be encoded.
	 *
	 * @param action          Name of Action to
	 * @param forward         Name of Global Action Forward
	 * @param href            Link which will be added to result without changes to its body
	 * @param anchor          Anchor to append to the end of URL
	 * @param parameters      Parameters to add to generated link
	 * @param request         HTTP Servlet request we're currently proceeding
	 * @param response        HTTP Servlet response we're creating
	 * @param encode          Whether or not to encode non-ASCII characters
	 * @param redirect        Whether or not we're generating an URL to use in <code>javax.servlet.http.HttpServletResponse#sendRedirect(String)</code> method
	 * @param addLocaleSuffix Whether or not to add default locale suffix
	 * @return Generated URL
	 * @throws MalformedURLException if URL is malformed, e.g. Global Action Forward with specified name could not be found
	 * @see javax.servlet.http.HttpServletResponse#sendRedirect(String)
	 */
	public static String computeURL(String action, String forward, String href, String anchor, Map parameters, HttpServletRequest request, HttpServletResponse response, boolean encode, boolean redirect, boolean addLocaleSuffix) throws MalformedURLException {
        return computeURL(action, forward, href, anchor, parameters, request, response, encode, redirect, addLocaleSuffix, false);
    }

	/**
	 * Computes URL from given values. Either <code>action</code> or <code>forward</code> or <code>href</code> must be non-null.
	 * Encodes all non-ASCII characters (if <code>encode</code> is true) and adds <code>jsessionid</code> to the generated link.
	 * If URL is absolute, it will not be encoded.
	 *
	 * @param action          Name of Action to
	 * @param forward         Name of Global Action Forward
	 * @param href            Link which will be added to result without changes to its body
	 * @param anchor          Anchor to append to the end of URL
	 * @param parameters      Parameters to add to generated link
	 * @param request         HTTP Servlet request we're currently proceeding
	 * @param response        HTTP Servlet response we're creating
	 * @param encode          Whether or not to encode non-ASCII characters
	 * @param redirect        Whether or not we're generating an URL to use in <code>javax.servlet.http.HttpServletResponse#sendRedirect(String)</code> method
	 * @param addLocaleSuffix Whether or not to add default locale suffix
     * @param noSessionId     Whether session ID should not be appended
	 * @return Generated URL
	 * @throws MalformedURLException if URL is malformed, e.g. Global Action Forward with specified name could not be found
	 * @see javax.servlet.http.HttpServletResponse#sendRedirect(String)
	 */
	public static String computeURL(String action, String forward, String href,
                                    String anchor, Map parameters,
                                    HttpServletRequest request,
                                    HttpServletResponse response,
                                    boolean encode, boolean redirect,
                                    boolean addLocaleSuffix,
                                    boolean noSessionId) throws MalformedURLException {
		Locale locale = WebappUtil.getEnsuredUserLocale(request);
		String language = null;
		if ( locale != null && addLocaleSuffix ) {
			language = locale.getLanguage();
		}
		return computeURL(action, forward, href, anchor, parameters, request,
                          response, encode, redirect, language, noSessionId);
	}

	/**
	 * Computes URL from given values. Either <code>action</code> or <code>forward</code> or <code>href</code> must be non-null.
	 * Encodes all non-ASCII characters (if <code>encode</code> is true) and adds <code>jsessionid</code> to the generated link.
	 * If URL is absolute, it will not be encoded.
	 *
	 * @param action       Name of Action to
	 * @param forward      Name of Global Action Forward
	 * @param href         Link which will be added to result without changes to its body
	 * @param anchor       Anchor to append to the end of URL
	 * @param parameters   Parameters to add to generated link
	 * @param request      HTTP Servlet request we're currently proceeding
	 * @param response     HTTP Servlet response we're creating
	 * @param encode       Whether or not to encode non-ASCII characters
	 * @param redirect     Whether or not we're generating an URL to use in <code>javax.servlet.http.HttpServletResponse#sendRedirect(String)</code> method
	 * @param localeSuffix Locale suffix to add
	 * @return Generated URL
	 * @throws MalformedURLException if URL is malformed, e.g. Global Action Forward with specified name could not be found
	 * @see javax.servlet.http.HttpServletResponse#sendRedirect(String)
	 */
	public static String computeURL(String action, String forward, String href,
                                    String anchor, Map parameters,
                                    HttpServletRequest request,
                                    HttpServletResponse response,
                                    boolean encode, boolean redirect,
                                    String localeSuffix) throws MalformedURLException {
        return computeURL(action, forward, href, anchor, parameters, request,
                          response, encode, redirect, localeSuffix, false);
    }

	/**
	 * Computes URL from given values. Either <code>action</code> or <code>forward</code> or <code>href</code> must be non-null.
	 * Encodes all non-ASCII characters (if <code>encode</code> is true) and adds <code>jsessionid</code> to the generated link.
	 * If URL is absolute, it will not be encoded.
	 *
	 * @param action       Name of Action to
	 * @param forward      Name of Global Action Forward
	 * @param href         Link which will be added to result without changes to its body
	 * @param anchor       Anchor to append to the end of URL
	 * @param parameters   Parameters to add to generated link
	 * @param request      HTTP Servlet request we're currently proceeding
	 * @param response     HTTP Servlet response we're creating
	 * @param encode       Whether or not to encode non-ASCII characters
	 * @param redirect     Whether or not we're generating an URL to use in <code>javax.servlet.http.HttpServletResponse#sendRedirect(String)</code> method
	 * @param localeSuffix Locale suffix to add
     * @param noSessionId  Whether session ID should not be appended
	 * @return Generated URL
	 * @throws MalformedURLException if URL is malformed, e.g. Global Action Forward with specified name could not be found
	 * @see javax.servlet.http.HttpServletResponse#sendRedirect(String)
	 */
	public static String computeURL(String action, String forward, String href,
            String anchor, Map parameters,
            HttpServletRequest request, HttpServletResponse response,
            boolean encode, boolean redirect, String localeSuffix,
            boolean noSessionId) throws MalformedURLException {

		// check correctness of attributes
		int n = 0;
		if ( action != null ) {
			n++;
		}
		if ( forward != null ) {
			n++;
		}
		if ( href != null ) {
			n++;
		}

		if ( n > 1 ) {
			throw new MalformedURLException("One and only one of 'action', 'forward' or 'href' attributes must be specified");
		} else if ( n == 0 ) {
			return null;
		}

		StringBuffer url = null;

		// flag to distinguish external URL from internal
		boolean urlIsExternal = false;

		if ( action != null ) {
			url = new StringBuffer(getActionMappingURL(action, null, request, WebappConstants.URL_TYPE_DOMAIN_RELATIVE, localeSuffix));
		} else if ( forward != null ) {
			String urlString = getActionForwardURL(forward, null, request, WebappConstants.URL_TYPE_DOMAIN_RELATIVE, localeSuffix);
			url = new StringBuffer(urlString);
			urlIsExternal = isExternalURL(urlString, request);
		} else if ( href != null ) {
			urlIsExternal = isExternalURL(href, request);
			url = new StringBuffer();

			// if href is internal URL, process it and insert locale suffix if it was requested
			if ( !urlIsExternal ) {
				// if href is internal URL and is not absolute, it means that it starts with slash
				if ( !isAbsoluteURL(href) ) {
					String contextPath = request.getContextPath();
					if ( !href.startsWith(contextPath) && !contextPath.equals("/") ) {
						url.append(contextPath);
					}
				}

				url.append(href);

				int questionMark = url.indexOf("?");
				String temp = url.toString();
				if ( questionMark != -1 ) {
					temp = url.substring(0, questionMark);
				}

				// add locale suffix if requested
				if ( localeSuffix != null && localeSuffix.trim().length() > 0 ) {
					String servletMapping = (String) request.getSession().getServletContext().getAttribute(Globals.SERVLET_KEY);
					String ext;
					if ( servletMapping != null && servletMapping.startsWith("*.") ) {
						ext = servletMapping.substring(1);
					} else {
						ext = ".jsp";
					}

					// dot before extension
					int extDot = temp.lastIndexOf(ext);
					if ( extDot < 0 && !ext.equals(".jsp") ) {
						extDot = temp.lastIndexOf(".jsp");
					}
					if ( extDot > 0 ) {

						// first insert an /rw prefix

						int fromIndex = 0;
						if ( isAbsoluteURL(href) ) {
							// look up scheme
							fromIndex = href.indexOf("://");
							if ( fromIndex < 0 ) {
								fromIndex = href.indexOf(":") + 1;
							} else {
								fromIndex += 3;
							}
						}

						String contextPath = request.getContextPath();
						if ( contextPath == null || contextPath.length() == 0 ) {
							contextPath = "/";
						}

						// save suffix pos for later use
						int suffixPos = extDot;

                        // length of text inserted in url to use correct positions
                        int correction = 0;

						// insert prefix if it does not exist
						int insertPos;
                        if ("/".equals(contextPath)) {
                            insertPos = url.indexOf(contextPath, fromIndex);
                        } else {
                            insertPos = url.indexOf(contextPath, fromIndex) + contextPath.length();
                        }

                        if ( url.indexOf(Constants.LOCALIZED_URI_PREFIX, insertPos) != insertPos ) {
							url.insert(insertPos, Constants.LOCALIZED_URI_PREFIX);
                            correction = Constants.LOCALIZED_URI_PREFIX.length();
							suffixPos += correction;
						}

						temp = temp.substring(0, extDot);

						// dot before language
						int langDot = temp.lastIndexOf(".");
						if ( langDot != -1 && langDot + correction == suffixPos - 3 ) {
							url.replace(langDot + correction, suffixPos, "." + localeSuffix);
						} else {
							url.insert(suffixPos, "." + localeSuffix);
						}
					}
				}
			} else {
				url.append(href);
			}
		}

		// add anchor (replace any existing)
		if ( anchor != null && anchor.trim().length() > 0 ) {
			int anchorPos = url.indexOf("#");
			if ( anchorPos > 0 ) {
				url.setLength(anchorPos);
			}
			url.append('#').append(anchor);
		}

		// attach query string
		Map resultParams = new HashMap();
		int k = url.indexOf("?");
		if ( k != -1 ) {
			String queryString = url.substring(k + 1);
			url.setLength(k);
			resultParams = RequestUtil.getRequestParametersFromString(queryString);
			if ( parameters != null ) {
				for ( Iterator i = parameters.entrySet().iterator(); i.hasNext(); ) {
					Map.Entry entry = (Map.Entry) i.next();
					String key = (String) entry.getKey();
					Object value = entry.getValue();
					resultParams.put(key, RequestUtil.mergeValues(resultParams.get(key), value));
				}
			}
		} else if ( parameters != null ) {
			resultParams.putAll(parameters);
		}

		if ( !resultParams.isEmpty() ) {
			// save any existing anchor
			int anchorPos = url.indexOf("#");
			if ( anchorPos > 0 ) {
				anchor = url.substring(anchorPos);
				url.setLength(anchorPos);
				if ( encode ) {
					try {
						anchor = URLEncoder.encode(anchor, Constants.DEFAULT_ENCODING);
					} catch ( UnsupportedEncodingException e ) {
						// do nothing
					}
				}
			} else {
				anchor = null;
			}

			url.append("?").append(RequestUtil.createQueryStringFromMap(resultParams, "&", encode && !urlIsExternal));

			// reappend anchor (if any)
			if ( anchor != null && anchor.trim().length() > 0 ) {
				url.append('#').append(anchor);
			}
		}

		String result = url.toString();

		if ( !urlIsExternal && !noSessionId ) {
			if ( redirect ) {
				result = response.encodeRedirectURL(result);
			} else {
				result = response.encodeURL(result);
			}
		}
		return result;
	}

    /**
     * Computes URL as external. Given URL is checked to start from our internal
     * context path (one that is obtained through a request object). If so,
     * internal context path is replaced with external one.
     *
     * @param href      href (internal)
     * @param secure    whether URL should be secure (use HTTPS) or not
     * @param request   request being processed
     * @return resulting URL
     */
    public static String computeExternalURL(String href, boolean secure,
            HttpServletRequest request) {
        String contextPath = request.getContextPath();
        ServletContext servletContext = request.getSession().getServletContext();
        GlobalProperties globalProperties = GlobalProperties.getInstance(servletContext);
        String externalContextPath = globalProperties.getString(WebappConstants.GLOBAL_PROPERTY_EXTERNAL_CONTEXT_PATH, "");
        String externalHost = globalProperties.getString(WebappConstants.GLOBAL_PROPERTY_EXTERNAL_HOST, "");
        Integer externalHttpPort = globalProperties.getInteger(WebappConstants.GLOBAL_PROPERTY_EXTERNAL_HTTP_PORT, 80);
        Integer externalHttpsPort = globalProperties.getInteger(WebappConstants.GLOBAL_PROPERTY_EXTERNAL_HTTPS_PORT, 443);
        
        StringBuffer buffer = new StringBuffer();
        // schema
        if (secure) {
            buffer.append("https://");
        } else {
            buffer.append("http://");
        }
        // host
        buffer.append(externalHost);
        // port
        if ((secure && externalHttpsPort.intValue() != 443)
                || (!secure && externalHttpPort.intValue() != 80)) {
            buffer.append(':').append(secure ? externalHttpsPort : externalHttpPort);
        }
        // context path
        if (href.startsWith(contextPath)) {
            if (!"".equals(externalContextPath) && !"/".equals(externalContextPath)) {
                buffer.append(externalContextPath);
            }
            // href
            if (!"".equals(contextPath) && !"/".equals(contextPath)) {
                href = href.substring(contextPath.length());
            }
        }
        buffer.append(href);
        return buffer.toString();
    }

    /**
	 * Gets field value from specified map of values. Map must contain mappings of localeIdentifiers to concrete values.
	 * While searching, following rules are applied:
	 * <ul>
	 * <li>
	 * 1. If map is null or is empty, null is returned.
	 * </li>
	 * <li>
	 * 2. Otherwise, searching for mapping for <code>localeIdentifier</code>
	 * </li>
	 * <li>
	 * 2.5 Following steps are applied only if <code>complexSearch</code> is
	 * <code>true</code>
	 * </li>
	 * <li>
	 * 3. If nothing has been found in step 2, searching for default locale for our application
	 * </li>
	 * <li>
	 * 4. If still not found, returning first mapping value from specified map
	 * </li>
	 * </ul>
	 *
	 * @param values           Map of values to search in
	 * @param localeIdentifier Locale identifier to get value for
	 * @param request          HTTP Servlet Request containing information about our Servlet Context
	 * @param complexSearch    Whether to use a bit more complex lookup algorithm
	 * @return Field value
	 */
	public static String getFieldValue(Map values, String localeIdentifier, HttpServletRequest request, boolean complexSearch) {

		if ( values == null || values.isEmpty() ) {
			return null;
		}

		String value = (String) values.get(localeIdentifier);
		if ( (value == null || value.length() == 0) && complexSearch ) {
			// nothing found for specified locale, search for default
			ContentLocale defaultLocale = LocaleUtil.getInstance(request.getSession().getServletContext()).getDefaultLocale();
			value = (String) values.get(defaultLocale.getIdentifier());

			if ( value == null || value.length() == 0 ) {
				// still not found, take first value in map
				value = (String) values.values().iterator().next();
			}
		}

		return value;
	}

	/**
	 * Replace extension in URI
	 *
	 * @param uri          The URI to replace extension in
	 * @param newExtension The new extension
	 * @return URI with replaced extension
	 */
	public static String changeUriExtension(String uri, String newExtension) {
		int slash = uri.lastIndexOf("/");
		int period = uri.lastIndexOf(".");
		if ( (period >= 0) && (period > slash) ) {
			String uriWithout = uri.substring(0, period);
			return uriWithout + newExtension;
		} else {
			return uri;
		}
	}


	/**
	 * Retrieves localized (if found) information string of specified localizable.
	 * For example, for layout it will be its name, for pages value of 'title'
	 * field for specified locale. If none was found for a specified locale,
	 * tries to search for value for default locale, if nothing found again --
	 * any locale may be used.
	 *
	 * @param localizableId ID of localizable to get information about
	 * @param request       HTTP Servlet Request containing information about our Servlet Context and locale under key
	 *                      <code>org.apache.struts.Globals.LOCALE_KEY</code>
	 * @return Information about localizable with specified ID
	 */
	public static String getLocalizableInfo(Long localizableId, HttpServletRequest request) {
		ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getSession().getServletContext());
		LookupManager lookupManager = (LookupManager) applicationContext.getBean(Constants.LOOKUP_MANAGER_BEAN);
		Localizable owner = lookupManager.retrieveLocalizable(localizableId);
		return getLocalizableInfo(owner, request);
	}

	/**
	 * Retrieves localized (if found) information string of specified localizable.
	 * For layout it will be its name, for pages value of 'title' field for
	 * specified locale. If none was found for a specified locale,
	 * tries to search for value for default locale, if nothing found again --
	 * any locale may be used.
     * Returned value is properly encoded.
	 *
	 * @param localizable Localizable to get information about
	 * @param request     HTTP Servlet Request containing information about our Servlet Context and locale under key
	 *                    <code>org.apache.struts.Globals.LOCALE_KEY</code>
	 * @return Information about specified localizable
	 */
	public static String getLocalizableInfo(Localizable localizable, HttpServletRequest request) {
		if ( localizable == null ) {
			return "";
		}
		String localizableInfo = "";
		if ( localizable instanceof Layout ) {
			Layout layout = (Layout) localizable;
			localizableInfo = layout.getName();
		} else if ( localizable instanceof Page ) {
			Map title = new HashMap();
			ContentField titleField = (ContentField) localizable.getContentFieldsMap().get("title");
			if ( titleField != null ) {
				for ( Iterator i = titleField.getContentFieldValues().iterator(); i.hasNext(); ) {
					ContentFieldValue contentFieldValue = (ContentFieldValue) i.next();
					title.put(contentFieldValue.getContentLocale().getIdentifier(), contentFieldValue.getSimpleValue());
				}
				Locale locale = RequestUtils.getUserLocale(request, null);
				localizableInfo = getFieldValue(title, locale.getLanguage(), request, true);
			}
		} else if ( localizable instanceof MailTemplate ) {
			MailTemplate mailTemplate = (MailTemplate) localizable;
			localizableInfo = mailTemplate.getIdentifier();
		}

        // encode HTML-sensitive chars
        localizableInfo = StringUtil.htmlEncode(localizableInfo);

		MessageResources resources = (MessageResources) request.getAttribute(Globals.MESSAGES_KEY);
		String ownerClassName = localizable.getClass().getName();
		// remove package
		int k = ownerClassName.lastIndexOf('.');
		ownerClassName = ownerClassName.substring(k + 1);
		String key = "core.localizable.form.class." + ownerClassName;
		Locale locale = RequestUtils.getUserLocale(request, null);

		String msg = resources.getMessage(locale, key, localizableInfo);
		return (msg == null || msg.length() == 0) ? localizableInfo : msg;
	}

	/**
	 * Returns <code>true</code> if map has non-null and non-empty values for all keys
	 *
	 * @param values Map of values to check
	 * @return True if map contains correct values
	 */
	public static boolean hasCorrectValues(Map values) {
        if (values == null)
            return false;
        boolean result = true;
		for ( Iterator i = values.keySet().iterator(); i.hasNext(); ) {
			String key = (String) i.next();
			String value = (String) values.get(key);
			result = result && (value != null && value.length() > 0);
		}
		return result;
	}

	/**
	 * <p>Copies properties from orig bean to dest for each case where property names are the same.
	 * Uses CommonConverter with appropriate locale stored in session to correctly convert date to string and vice versa.
	 * </p>
	 * <p>This method is synchronized, so it guarantees, that correct converters will be set for each request
	 * </p>
	 *
	 * @param dest    Destination bean to copy properties to
	 * @param orig    Original bean to copy properties from
	 * @param request Http servlet request we are processing
     * @throws IllegalAccessException if illegal access occured during copying
     * @throws java.lang.reflect.InvocationTargetException if error occurs
     * during copiing 
	 * @see com.blandware.atleap.common.util.CommonConverter#CommonConverter(java.util.Locale)
	 */
	public static synchronized void copyProperties(Object dest, Object orig, HttpServletRequest request) throws IllegalAccessException, InvocationTargetException {

		boolean requestSpecified = request != null;

		CommonConverter converter = new CommonConverter();

		if ( requestSpecified ) {
			Locale locale = RequestUtils.getUserLocale(request, null);
			converter = new CommonConverter(locale);
		}

		ConvertUtils.register(converter, Date.class);
		ConvertUtils.register(converter, String.class);

		BeanUtils.copyProperties(dest, orig);

		if ( requestSpecified ) {
			// Register default converters
			ConvertUtils.register(new CommonConverter(), String.class);
			ConvertUtils.register(new CommonConverter(), Date.class);
		}

	}

	/**
	 * <p>Copies properties from orig bean to dest for each case where property names are the same.
	 * Uses CommonConverter with default locale to convert date to string and vice versa.
	 * </p>
	 * <p>This method is synchronized, so it guarantees, that correct converters will be set for each request
	 * </p>
	 *
	 * @param dest Destination bean to copy properties to
	 * @param orig Original bean to copy properties from
     * @throws IllegalAccessException if illegal access occured during copying
     * @throws java.lang.reflect.InvocationTargetException if error occurs
     * during copiing
	 * @see com.blandware.atleap.common.util.CommonConverter#CommonConverter()
	 */
	public static synchronized void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
		copyProperties(dest, orig, null);
	}

    /**
     * Returns set of indexed overridable field identifiers for specified
     * Localizable
     *
     * @param owner Localizable to retrieve allowed field identifiers for
     * @param request HttpServletRequest we are currently proceeding
     * @return Set of allowed identifiers
     * @throws DefinitionsFactoryException if some error has occured when trying to get parent layout of page's owner layout
     */
    public static Set getIndexedOverridableFieldIdentifiers(Localizable owner, HttpServletRequest request) throws DefinitionsFactoryException {
        return getOverridableFieldIdentifiers(owner, request, true, true);
    }

    /**
     * Returns set of indexed overridable field identifiers for specified
     * Localizable
     *
     * @param owner Localizable to retrieve allowed field identifiers for
     * @param request HttpServletRequest we are currently proceeding
     * @return Set of allowed identifiers
     * @throws DefinitionsFactoryException if some error has occured when trying to get parent layout of page's owner layout
     */
    public static Set getUnIndexedOverridableFieldIdentifiers(Localizable owner, HttpServletRequest request) throws DefinitionsFactoryException {
        return getOverridableFieldIdentifiers(owner, request, true, false);
    }

	/**
	 * Returns set of overridable field identifiers for specified Localizable,
     * no matter whether they are indexed or not
	 *
	 * @param owner   Localizable to retrieve allowed field identifiers for
	 * @param request HttpServletRequest we are currently proceeding
	 * @return Set of allowed identifiers
	 * @throws DefinitionsFactoryException if some error has occured when trying to get parent layout of page's owner layout
	 */
    public static Set getOverridableFieldIdentifiers(Localizable owner, HttpServletRequest request) throws DefinitionsFactoryException {
        return getOverridableFieldIdentifiers(owner, request, false, false);
    }

	/**
	 * Returns set of overridable field identifiers for specified Localizable
	 *
	 * @param owner   Localizable to retrieve allowed field identifiers for
	 * @param request HttpServletRequest we are currently proceeding
     * @param considerIndexed if this is false, then indexed and unindexed
     * fields will be returned; indexed parameter is ignored. Else indexed
     * parameter controlls what fields will be considered: indexed or unindexed.
     * @param indexed whether to return indexed or unindexed fields identifiers.
     * If considerIndexed is false, this is ignored.
	 * @return Set of allowed identifiers
	 * @throws DefinitionsFactoryException if some error has occured when trying to get parent layout of page's owner layout
	 */
	public static Set getOverridableFieldIdentifiers(Localizable owner, HttpServletRequest request, boolean considerIndexed, boolean indexed) throws DefinitionsFactoryException {

		ServletContext servletContext = request.getSession().getServletContext();
		ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
		LayoutManager layoutManager = (LayoutManager) applicationContext.getBean(Constants.LAYOUT_MANAGER_BEAN);

        // First obtain list of all field identifiers from layouts that relate
        // to a given Localizable
		Set fieldIdentifiers = new HashSet();
		String layoutDefinition;
		if ( owner instanceof ContentPage || owner instanceof Layout ) {
            // For content page obtain identifiers of all fields from its
            // layout and all its parents; for layout obtain identifiers of
            // fields from it and all its parents
			if ( owner instanceof ContentPage ) {
				ContentPage contentPage = (ContentPage) owner;
				layoutDefinition = contentPage.getLayout().getDefinition();
			} else {
				Layout layout = (Layout) owner;
				layoutDefinition = layout.getDefinition();
			}
			do {
				Layout layout = layoutManager.findLayoutByDefinition(layoutDefinition);
				if ( layout != null && (!(owner instanceof Layout) || !layout.equals(owner)) ) {
                    // for layout, consider only fields from its parents
					fieldIdentifiers.addAll(layout.getContentFieldsMap().keySet());
				}
				layoutDefinition = ((HeritableComponentDefinition) TilesUtil.getDefinition(layoutDefinition, request, servletContext)).getExtends();
			} while ( layoutDefinition != null );
		} else if ( owner instanceof ActionPage ) {
            // For action page obtain identifiers of all fields from all layouts
			PartialCollection layouts = layoutManager.listLayouts(null);
			for ( Iterator i = layouts.iterator(); i.hasNext(); ) {
				Layout layout = (Layout) i.next();
				fieldIdentifiers.addAll(layout.getContentFieldsMap().keySet());
			}
		} else {
			return new HashSet();
		}

        // Skip some fields
		Set identifiers = new HashSet();
		for ( Iterator i = fieldIdentifiers.iterator(); i.hasNext(); ) {
			String identifier = (String) i.next();

			int k = identifier.indexOf('[');
			if ( k != -1 ) {
				identifier = identifier.substring(0, k);
			}
			if ( owner instanceof ContentPage || owner instanceof ActionPage ) {
				ContentField pageField = (ContentField) owner.getContentFieldsMap().get(identifier);
				if ( pageField != null && pageField.getInternal().booleanValue() ) {
                    // Skip internal fields for ContentPage or ActionPage
					continue;
				}
			}
            boolean fieldIsIndexed = (k != -1);
            if (considerIndexed) {
                if ((fieldIsIndexed && !indexed) || (!fieldIsIndexed && indexed)) {
                    continue;
                }
            }

			identifiers.add(identifier);
		}
		return identifiers;
	}

    /**
     * Returns a set of field identifiers for field which belong explicitly to
     * a given Localizable. Only identifiers for unindexed fields are returned.
     *
     * @param localizable Localizable from which to obtain set of identifiers
     * @return set of field identifiers
     */
    public static Set getLocalizableUnIndexedFieldsIdentifiers(Localizable localizable) {
        Set result = new TreeSet();
        List fields = localizable.getContentFields();
        for (int i = 0; i < fields.size(); i++) {
            ContentField field = (ContentField) fields.get(i);
            if (field.getIndex() == null) {
                result.add(field.getIdentifierWithoutIndex());
            }
        }
        return result;
    }

	/**
	 * Returns set of indexed field identifiers
	 *
	 * @param layoutDefinition Definition of layout to search fields on (all parent layouts will also be searched)
	 * @param request          HttpServletRequest we are currently processing
	 * @return Set of indexed identifiers
	 * @throws DefinitionsFactoryException if some error has occured when trying to get parent layout of page's layout layout
	 */
	public static Set getIndexedFieldIdentifiers(String layoutDefinition, HttpServletRequest request) throws DefinitionsFactoryException {
		return getFieldIdentifiers(layoutDefinition, request, true);
	}

	/**
	 * Returns set of unindexed field identifiers
	 *
	 * @param layoutDefinition Definition of layout to search fields on (all parent layouts will also be searched)
	 * @param request          HttpServletRequest we are currently processing
	 * @return Set of unindexed identifiers
	 * @throws DefinitionsFactoryException if some error has occured when trying to get parent layout of page's layout layout
	 */
	public static Set getUnIndexedFieldIdentifiers(String layoutDefinition, HttpServletRequest request) throws DefinitionsFactoryException {
		return getFieldIdentifiers(layoutDefinition, request, false);
	}

    /**
	 * Returns set of fields' identifiers for layout with specified definition
	 *
	 * @param layoutDefinition Definition of layout to search fields on
	 * @param request          HttpServletRequest we are currently processing
	 * @param indexed          If true, indexed field identifiers will be returned, otherwise - unindexed
	 * @return Set of identifiers
	 * @throws DefinitionsFactoryException if some error has occured when trying to get parent layout of page's layout layout
	 */
	private static Set getFieldIdentifiers(String layoutDefinition, HttpServletRequest request, boolean indexed) throws DefinitionsFactoryException {
		ServletContext servletContext = request.getSession().getServletContext();
		ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
		LayoutManager layoutManager = (LayoutManager) applicationContext.getBean(Constants.LAYOUT_MANAGER_BEAN);

		Set fieldIdentifiers = new HashSet();

		do {
			Layout layout = layoutManager.findLayoutByDefinition(layoutDefinition);
			if ( layout != null ) {
				fieldIdentifiers.addAll(layout.getContentFieldsMap().keySet());
			}
			layoutDefinition = ((HeritableComponentDefinition) TilesUtil.getDefinition(layoutDefinition, request, servletContext)).getExtends();
		} while ( layoutDefinition != null );

		Set identifiers = new HashSet();
		for ( Iterator i = fieldIdentifiers.iterator(); i.hasNext(); ) {
			String identifier = (String) i.next();
			int k = identifier.indexOf('[');
			if ( k != -1 ) {
				identifier = identifier.substring(0, k);
				if ( indexed ) {
					identifiers.add(identifier);
				}
			} else {
				if ( !indexed ) {
					identifiers.add(identifier);
				}
			}
		}
		return identifiers;
	}

    /**
     * Returns a set of sequence identifiers for sequences which belong
     * explicitly to a given Localizable.
     *
     * @param localizable Localizable from which to obtain set of identifiers
     * @return set of sequence identifiers
     */
    public static Set getLocalizableSequenceIdentifiers(Localizable localizable) {
        Set result = new HashSet();
        result.addAll(localizable.getSequencesMap().keySet());
        return result;
    }

	/**
	 * Returns set of overridable sequence identifiers for specified Localizable.
	 *
	 * @param owner   Localizable to retrieve allowed sequence identifiers for
	 * @param request HttpServletRequest we are currently proceeding
	 * @return Set of allowed identifiers
	 * @throws DefinitionsFactoryException if some error has occured when trying to get parent layout of page's owner layout
	 */
	public static Set getOverridableSequenceIdentifiers(Localizable owner, HttpServletRequest request) throws DefinitionsFactoryException {

		ServletContext servletContext = request.getSession().getServletContext();
		ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
		LayoutManager layoutManager = (LayoutManager) applicationContext.getBean(Constants.LAYOUT_MANAGER_BEAN);

        // First obtain list of all sequence identifiers from layouts that relate
        // to a given Localizable
		Set sequenceIdentifiers = new HashSet();
		String layoutDefinition;
		if ( owner instanceof ContentPage || owner instanceof Layout ) {
            // For content page obtain identifiers of all sequences from its
            // layout and all its parents; for layout obtain identifiers of
            // sequences from it and all its parents
			if ( owner instanceof ContentPage ) {
				ContentPage contentPage = (ContentPage) owner;
				layoutDefinition = contentPage.getLayout().getDefinition();
			} else {
				Layout layout = (Layout) owner;
				layoutDefinition = layout.getDefinition();
			}
			do {
				Layout layout = layoutManager.findLayoutByDefinition(layoutDefinition);
				if ( layout != null && (!(owner instanceof Layout) || !layout.equals(owner)) ) {
                    // for layout, consider only sequences from its parents
					sequenceIdentifiers.addAll(layout.getSequencesMap().keySet());
				}
				layoutDefinition = ((HeritableComponentDefinition) TilesUtil.getDefinition(layoutDefinition, request, servletContext)).getExtends();
			} while ( layoutDefinition != null );
		} else if ( owner instanceof ActionPage ) {
            // For action page obtain identifiers of all sequences from all layouts
			PartialCollection layouts = layoutManager.listLayouts(null);
			for ( Iterator i = layouts.iterator(); i.hasNext(); ) {
				Layout layout = (Layout) i.next();
				sequenceIdentifiers.addAll(layout.getSequencesMap().keySet());
			}
		} else {
			return new HashSet();
		}

        return sequenceIdentifiers;
    }

    /**
	 * Checks charset for support
	 *
	 * @param charset to check
	 * @return <code>true</code> if supported
	 */
	public static boolean isCharsetSupported(String charset) {
		boolean result = false;
		try {
			result = Charset.isSupported(charset);
		} catch ( Exception e ) {
			//do nothing
		}
		return result;
	}

	/**
	 * Looks up for base object (in common case page or resource) with specified URI
	 *
	 * @param ref            URI to lookup base object
	 * @param servletContext Servlet context of our application
	 * @param contextPath    Application context path
	 * @return Base object or <code>null</code> if none was found or URI is external
	 */
    public static BaseObject lookupObject(String ref, ServletContext servletContext, String contextPath) {
        return lookupObject(ref, servletContext, contextPath, null);
    }

	/**
	 * Looks up for base object (in common case page or resource) with
     * specified URI.
	 *
	 * @param ref            URI to lookup base object
	 * @param servletContext Servlet context of our application
	 * @param contextPath    Application context path
     * @param request        HTTP request which allows to determine whether
     *                       given ref is external (if <code>null</code>, the
     *                       ref is assumed to be internal)
	 * @return Base object or <code>null</code> if none was found or URI
     * is external
	 */
	public static BaseObject lookupObject(String ref,
                                          ServletContext servletContext,
                                          String contextPath,
                                          HttpServletRequest request) {
        BaseObject obj;
        String uri = extractObjectUri(ref, servletContext,
                                      contextPath, request);
        if (uri == null) {
            return null;
        }
        ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);

		if ( uri.startsWith(Constants.RESOURCES_URI_PREFIX) ) {
			// look up for resource
			ContentResourceManager contentResourceManager = (ContentResourceManager) applicationContext.getBean(Constants.CONTENT_RESOURCE_MANAGER_BEAN);
			obj = contentResourceManager.findContentResourceByUri(uri);
		} else {
			// look up for page
			PageManager pageManager = (PageManager) applicationContext.getBean(Constants.PAGE_MANAGER_BEAN);
			obj = pageManager.findPageByUri(uri);
        }

		return obj;
	}

    /**
     * <p>Takes an URL which probably points to a resource or page and returns
     * that object URI (or <code>null</code> if URL does not contain a valid
     * object URI). Such URI may be used to retrieve an object.</p>
     * <p>No check is made whether such object actually exists or not.</p>
     *
	 * @param ref            URL from which to extract object URI
	 * @param servletContext Servlet context
     * @param contextPath    Application context path
     * @param request        HTTP request which allows to determine whether
     *                       given ref is external (if <code>null</code>, the
     *                       ref is assumed to be internal)
     * @return object URI or <code>null</code>
     */
    public static String extractObjectUri(String ref,
                                          ServletContext servletContext,
                                          String contextPath,
                                          HttpServletRequest request) {
		if ( ref == null || ref.trim().length() == 0 ) {
			return null;
		}

        if (request != null) {
            if (ref.toLowerCase().startsWith("http://") || ref.toLowerCase().startsWith("https://")) {
                if (isExternalURL(ref, request)) {
                    return null;
                }
            }
        }

		String uri = ref;

		// check for scheme. it must be http or https, otherwise URI is external
		if ( uri.startsWith("http") ) {
			String scheme;
			if ( uri.startsWith("https") ) {
				scheme = "https://";
			} else {
				scheme = "http://";
			}
			uri = uri.substring(scheme.length());

			// remove everything before next slash (typically host and,
            // probably, port)
			int k = uri.indexOf("/");
			if ( k != -1 ) {
				uri = uri.substring(k);
			}

		}

		// remove context path
		if ( uri.startsWith(contextPath) && !contextPath.equals("/") ) {
			uri = uri.substring(contextPath.length());
		}

		// add starting slash
		if ( !uri.startsWith("/") ) {
			uri = "/" + uri;
		}

		// remove query string
		int k = uri.indexOf("?");
		if ( k != -1 ) {
			uri = uri.substring(0, k);
		}

		// now we can look up for object
		if ( uri.startsWith(Constants.RESOURCES_URI_PREFIX) ) {
			// resource
		} else {
			// page

            // it may begin with /rw prefix, so cut it off (if it's not a news
            // item or something like, because they have their own prefixes)
            if (uri.startsWith(Constants.LOCALIZED_URI_PREFIX + "/")) {
                boolean special = uriStartsWithSpecialPrefix(uri, servletContext);
                if (!special) {
                    uri = uri.substring(Constants.LOCALIZED_URI_PREFIX.length());
                }
            }

			// remove extension
			k = uri.lastIndexOf(".");
			if ( k != -1 ) {
				uri = uri.substring(0, k);
			}

			// check for locale suffix
			k = uri.lastIndexOf(".");
			if ( k != -1 && k == uri.length() - 3 ) {
				uri = uri.substring(0, k);
			}
		}

		return uri;
    }

    /**
	 * Maps set of given references (URIs) to corresponding base objects (pages and/or resources).
	 * If no object has been found, ref is ignored.
	 *
	 * @param refs           Set of refs to lookup objects for
	 * @param servletContext Servlet context of our application
	 * @param contextPath    Application context path
	 * @return Map with pairs <code>ref -&gt; object</code>. Map can be empty if all references are external or point to
	 *         non-existing objects
	 */
	public static Map mapObjectsToRefs(Set refs, ServletContext servletContext, String contextPath) {
		Map linkedObjects = new HashMap();
		for ( Iterator i = refs.iterator(); i.hasNext(); ) {
			String ref = (String) i.next();
			BaseObject linkedObject = lookupObject(ref, servletContext, contextPath);
			if ( linkedObject != null ) {
				linkedObjects.put(ref, linkedObject);
			}
		}
		return linkedObjects;
	}

	/**
	 * Returns <code>true</code> if user has at least one role (if <code>all</code> is <code>false</code>)
	 * or all roles (if <code>all</code> is <code>true</code>), specified in collection
	 *
	 * @param roles    Roles to check
	 * @param all      Whether or not user must have all roles, specified in collection
	 * @param property Property of bean in list to get role name
	 * @param request  HTTP servlet request we're currently proceeding
	 * @return <code>True</code> if user has role, <code>false</code> otherwise.
     * @throws IllegalAccessException if illegal access occured during
     * calculating property value
     * @throws NoSuchMethodException if no method is found during computing
     * property value
     * @throws java.lang.reflect.InvocationTargetException if error occurs
     * during computing property value
	 */
	public static boolean isUserInRole(Collection roles, boolean all, String property, HttpServletRequest request) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
		boolean isUserInRole = all;
		if ( roles != null && !roles.isEmpty() ) {
			Iterator rolesIterator = roles.iterator();
			while ( rolesIterator.hasNext() ) {
				Object bean = rolesIterator.next();
				if ( bean == null ) {
					continue;
				}
				String roleName;
				if ( bean instanceof String ) {
					roleName = (String) bean;
				} else if ( property != null && property.trim().length() > 0 ) {
					roleName = String.valueOf(PropertyUtils.getProperty(bean, property));
				} else {
					roleName = String.valueOf(bean);
				}
				boolean roleCheck = request.isUserInRole(roleName.trim());
				if ( all ) {
					isUserInRole = isUserInRole && roleCheck;
				} else {
					isUserInRole = isUserInRole || roleCheck;
				}
			}
		} else {
			// if no role is specified, grant access
			isUserInRole = true;
		}
		return isUserInRole;
	}

	/**
	 * Returns <code>true</code> if user has at least one role (if <code>all</code> is <code>false</code>)
	 * or all roles (if <code>all</code> is <code>true</code>), specified in collection
	 *
	 * @param roles   Roles to check
	 * @param all     Whether or not user must have all roles, specified in collection
	 * @param request HTTP servlet request we're currently proceeding
	 * @return <code>True</code> if user has role, <code>false</code> otherwise.
	 */
	public static boolean isUserInRole(Collection roles, boolean all, HttpServletRequest request) {
		boolean isUserInRole = false;
		try {
			isUserInRole = isUserInRole(roles, all, null, request);
		} catch ( Exception ex ) {
			// do nothing
		}
		return isUserInRole;
	}

	/**
	 * Returns array of role names which are granted to gain access to action page with specified URI
	 *
	 * @param uri     URI of action page to lookup role names for
	 * @param request HTTP servlet request we're currently proceeding
	 * @return Array of role names
	 */
	public static String[] getAPRoleNames(String uri, HttpServletRequest request) {
		ModuleConfig moduleConfig = ModuleUtils.getInstance().getModuleConfig(request, request.getSession().getServletContext());
		ActionMapping mapping = (ActionMapping) moduleConfig.findActionConfig(uri);
		if ( mapping == null ) {
			return null;
		} else {
			return mapping.getRoleNames();
		}
	}

	/**
	 * Returns list of role names which are granted to gain access to action page with specified URI
	 *
	 * @param uri     URI of action page to lookup role names for
	 * @param request HTTP servlet request we're currently proceeding
	 * @return List of role names
	 */
	public static List getAPRoleNamesAsList(String uri, HttpServletRequest request) {
		String[] roles = getAPRoleNames(uri, request);
		if ( roles == null ) {
			return null;
		} else {
			return Arrays.asList(roles);
		}
	}

	/**
	 * Returns <code>true</code> if specified URL is absolute and
	 * <code>false</code> otherwise.
	 * Absolute URL is the URL which starts with the scheme, colon sign and sequence of two slashes
	 *
	 * @param url URL to check
	 * @return <code>true</code> if specified URL is absolute and <code>false</code> otherwise.
	 */
	public static boolean isAbsoluteURL(String url) {
		// a null URL is not absolute
		if ( url == null ) {
			return false;
		}

		// valid scheme starts from a sequence of valid scheme chars, colon sign and sequence of two slashes
		int colonPos = url.indexOf(":");
		if ( colonPos != -1 && url.startsWith("//", colonPos + 1) ) {
			// colon sign exists in string, such as sequence of two slashes
			// check sequence of characters before colon to be the valid scheme
			String scheme = url.substring(0, colonPos);
			return isValidScheme(scheme);
		} else {
			return false;
		}

	}

	/**
	 * Returns <code>true</code> if URL is external and <code>false</code> otherwise.<br />
	 * URL is external if any of the following conditions are met:
	 * <ul>
	 * <li>URL is not absolute and does not start with the slash</li>
	 * <li>URL is absolute and host name does not equal to the host name of server, we're running on</li>
	 * <li>URL is absolute and our port number is not standard (is not 80 nor 443) and is not present in URL</li>
	 * <li>URL is absolute and context path of our application is not present in URL</li>
	 * </ul>
	 *
	 * @param url     URL to check
	 * @param request HTTP Servlet request we're currently proceeding
	 * @return <code>true</code> if URL is external and <code>false</code> otherwise
	 */
	public static boolean isExternalURL(String url, HttpServletRequest request) {

		String temp;

        if (isJavascriptURL(url)) {
            return true;
        }

        // get our host
		String host = request.getServerName();
		boolean urlIsAbsolute = isAbsoluteURL(url);

		if ( urlIsAbsolute ) {

			// remove scheme and sequence of two slashes from the start of the string
			int schemeEnd = url.indexOf("//");

			// because uri is absolute, schemeEnd cannot be -1
			temp = url.substring(schemeEnd + 2);

            boolean isHttps;
            String scheme = url.substring(0, schemeEnd);
            isHttps = "https:".equalsIgnoreCase(scheme);
            if (!isHttps) {
                if (!"http:".equalsIgnoreCase(scheme)) {
                    // unknown scheme, URL is external
                    return true;
                }
            }

            int firstSlash = temp.indexOf("/");
            if (firstSlash == -1) {
                firstSlash = temp.length();
            }
            int firstColon = temp.indexOf(":");
            if (firstColon == -1) {
                firstColon = temp.length();
            }
            int firstNonHostChar = firstColon < firstSlash ? firstColon : firstSlash;
			if ( !temp.substring(0, firstNonHostChar).equals(host) ) {
				return true;
			}
			// host is the same, check port
            ServletContext servletContext = request.getSession().getServletContext();
            String portStr = isHttps
                             ? servletContext.getInitParameter(SslUtil.HTTPS_PORT_PARAM)
                             : servletContext.getInitParameter(SslUtil.HTTP_PORT_PARAM);
            int port = Integer.valueOf(portStr).intValue();
			// port is custom if it is not equal to 80 for HTTP nor to 443 for HTTPS
			boolean standardPort = !isHttps ? (port == 80) : (port == 443);
			temp = temp.substring(host.length());
			if ( temp.startsWith(":") ) {
				temp = temp.substring(1);
				if ( !temp.startsWith(portStr) ) {
					return true;
				}
				temp = temp.substring(portStr.length());
			} else if ( !standardPort ) {
				return true;
			}

			// port test passed, check context path
			String contextPath = request.getContextPath();
            return !temp.startsWith(contextPath);
		} else {
			// URL is not absolute, so search for slash at the beginning
			return !url.startsWith("/");
		}
	}

    /**
     * Returns whether URL represents Javascript code.
     *
     * @param url URL to check
     * @return true if this is javascript code
     */
    public static boolean isJavascriptURL(String url) {
        return (url != null && url.toLowerCase().startsWith("javascript:"));
    }

    /**
     * <p>Ensures that user locale is stored in session under certain key
     * (currently Globals.LOCALE_KEY), presents in system and active, or just
     * cannot be changed.
     * </p>
     * <p>
     * Firstly an attribute under special key
     * (WebappConstants.LOCALE_WAS_CHECKED_KEY) is checked. If it presents in a
     * request, that means that check was already done during this request. If
     * no such key presents, than locale is checked.
     * </p>
     * <p>During this process, it is checked that user locale can be changed.
     * If not, nothing is done more, but flag in request is set. If locale can
     * be changed, than it's obtained from parameter 'language', from session
     * or from request. After that, found locale if checked to be existent
     * active locale. If it's not, default locale is taken.
     * </p>
     * <p>When locale is determined, it's put into session.
     * </p>
     *
     * @param request request
     */
    public static void ensureLocaleValidity(HttpServletRequest request) {

        if (request.getAttribute(WebappConstants.LOCALE_WAS_CHECKED_KEY) != null) {
            return;
        }

        request.setAttribute(WebappConstants.LOCALE_WAS_CHECKED_KEY,
                             Boolean.TRUE);

        String localeIdentifier;
        HttpSession session = request.getSession();
        ServletContext servletContext = session.getServletContext();

        // check the presence of 'language' parameter in request
        localeIdentifier = request.getParameter("language");

        // can we automatically change locale?
        ModuleConfig moduleConfig = ModuleUtils.getInstance().getModuleConfig(null, request, servletContext);
        boolean canChangeLocale = moduleConfig.getControllerConfig().getLocale();
        if ( (localeIdentifier == null || localeIdentifier.trim().length() == 0) && !canChangeLocale ) {
            return;
        }

        // 'language' parameter in request has the top priority
        // if it is not specified, check session and request
        if ( localeIdentifier == null || localeIdentifier.trim().length() == 0 ) {
            Locale locale = (Locale) session.getAttribute(Globals.LOCALE_KEY);
            if ( locale == null ) {
                // Use the Locale returned by the servlet container (if any)
                locale = request.getLocale();
            }
            if ( locale != null ) {
                localeIdentifier = locale.getLanguage();
            }
        }

        LocaleUtil localeUtil = LocaleUtil.getInstance(servletContext);
        if ( localeIdentifier != null && localeIdentifier.trim().length() > 0 ) {
            List availableLocales = localeUtil.getAvailableLocales();
            ContentLocale contentLocale;
            boolean localeFound = false;
            boolean localeIsActive = false;
            for ( Iterator i = availableLocales.iterator(); i.hasNext() && !localeFound; ) {
                contentLocale = (ContentLocale) i.next();
                localeFound = contentLocale.getIdentifier().equalsIgnoreCase(localeIdentifier);
                localeIsActive = contentLocale.getActive().booleanValue();
            }
            if ( !localeFound || !localeIsActive ) {
                contentLocale = localeUtil.getDefaultLocale();
                if ( contentLocale != null ) {
                    localeIdentifier = contentLocale.getIdentifier();
                }
            }
        } else {
            ContentLocale contentLocale = localeUtil.getDefaultLocale();
            if ( contentLocale != null ) {
                localeIdentifier = contentLocale.getIdentifier();
            }
        }

        session.setAttribute(Globals.LOCALE_KEY, new Locale(localeIdentifier));
    }

    /**
     * Returns user locale. Before obtaining locale itself, it's ensured that
     * it exists in session and that it is an existent and active locale.
     *
     * @param request request
     * @return the locale
     */
    public static Locale getEnsuredUserLocale(HttpServletRequest request) {
        ensureLocaleValidity(request);
        return RequestUtils.getUserLocale(request, null);
    }

    /**
     * Prepares list of roles to be output using javascript. Double quotes in
     * title are escaped, description is converted to single line (new line
     * symbols are replaced with spaces).
     *
     * @param roles list of roles
     * @return new list
     * @throws IllegalAccessException if illegal access occured during copying
     * @throws java.lang.reflect.InvocationTargetException if error occurs
     * during copiing
     */
    public static List prepareRolesListForJs(List roles)
            throws IllegalAccessException, InvocationTargetException {
        List result = new ArrayList();
        for (Iterator i = roles.iterator(); i.hasNext(); ) {
            Role role = (Role) i.next();
            Role copy = new Role();
            copyProperties(copy, role);
            copy.setTitle(StringUtil.escapeDoubleQuotesForJs(copy.getTitle()));
            copy.setDescription(StringUtil.asSingleLine(copy.getDescription()));
            result.add(copy);
        }
        return result;
    }

    /**
     * Prepares list of groups to be output using javascript. Double quotes in
     * title are escaped, description is converted to single line (new line
     * symbols are replaced with spaces).
     *
     * @param groups list of groups
     * @return new list
     * @throws IllegalAccessException if illegal access occured during copying
     * @throws java.lang.reflect.InvocationTargetException if error occurs
     * during copiing
     */
    public static List prepareGroupsListForJs(List groups)
            throws IllegalAccessException, InvocationTargetException {
        List result = new ArrayList();
        for (Iterator i = groups.iterator(); i.hasNext(); ) {
            Group group = (Group) i.next();
            Group copy = new Group();
            copyProperties(copy, group);
            copy.setTitle(StringUtil.escapeDoubleQuotesForJs(copy.getTitle()));
            copy.setDescription(StringUtil.asSingleLine(copy.getDescription()));
            result.add(copy);
        }
        return result;
    }

    /**
     * For each locale which presents in system, if value in map for it does not
     * exists (or empty) and is not in old field value, copies value from value
     * for default locale.
     *
     * @param fieldValue    mapping from locale identifiers to values
     * @param oldFieldValue existing mapping from locale identifiers to values 
     * @param servletContext servlet context
     */
    public static void fillFieldValueWithDefaultLocaleValue(Map fieldValue,
            Map oldFieldValue, ServletContext servletContext) {
        LocaleUtil localeUtil = LocaleUtil.getInstance(servletContext);
        List availableLocales = localeUtil.getAvailableLocales();
        String defaultLocale = localeUtil.getDefaultLocale().getIdentifier();
        String defaultLocaleValue = (String) fieldValue.get(defaultLocale);
        Iterator i = availableLocales.iterator();
        while (i.hasNext()) {
            ContentLocale contentLocale = (ContentLocale) i.next();
            String localeIdentifier = contentLocale.getIdentifier();
            boolean oldValueExists = !GenericValidator.isBlankOrNull((String) oldFieldValue.get(localeIdentifier));
            boolean newValueExists = !GenericValidator.isBlankOrNull((String) fieldValue.get(localeIdentifier));
            if (!newValueExists) {
                if (oldValueExists) {
                    fieldValue.put(localeIdentifier, oldFieldValue.get(localeIdentifier));
                } else {
                    fieldValue.put(localeIdentifier, defaultLocaleValue);
                }
            }
        }
    }

    /**
     * Copies values from one field values map to another. All values existing
     * in src map are copied to dest map, but values that are in dest but not in
     * src map are preserved.
     *
     * @param destFieldValue destination mapping from locale identifiers to values
     * @param srcFieldValue  source mapping from locale identifiers to values
     */
    public static void copyFieldValue(Map destFieldValue, Map srcFieldValue) {
        Iterator i = srcFieldValue.entrySet().iterator();
        while (i.hasNext()) {
            Map.Entry entry = (Map.Entry) i.next();
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            destFieldValue.put(key, value);
        }
    }

    /**
	 * Determines if the character is allowed in the scheme of an URI.
	 * RFC 2396 "Uniform Resource Identifiers: Generic Syntax" says the following (Section 3.1 "Scheme Component"):
	 * <blockquote>
	 * Scheme names consist of a sequence of characters beginning with a
	 * lower case letter and followed by any combination of lower case
	 * letters, digits, plus ("+"), period ("."), or hyphen ("-").  For
	 * resiliency, programs interpreting URI should treat upper case letters
	 * as equivalent to lower case in scheme names (e.g., allow "HTTP" as
	 * well as "http").
	 * <p/>
	 * </blockquote>
     *
     * @param c character to check
     * @return true if character is allowed in scheme
     */
	protected static boolean isValidSchemeChar(char c) {
		return Character.isLetterOrDigit(c) || c == '+' || c == '-' || c == '.';
	}

	/**
	 * Checks given scheme for validity.
	 * RFC 2396 "Uniform Resource Identifiers: Generic Syntax" says the following (Section 3.1 "Scheme Component"):
	 * <blockquote>
	 * Scheme names consist of a sequence of characters beginning with a
	 * lower case letter and followed by any combination of lower case
	 * letters, digits, plus ("+"), period ("."), or hyphen ("-").  For
	 * resiliency, programs interpreting URI should treat upper case letters
	 * as equivalent to lower case in scheme names (e.g., allow "HTTP" as
	 * well as "http").
	 * <p/>
	 * </blockquote>
	 *
	 * @param scheme Scheme to check for validity
	 * @return <code>true</code> if scheme is valid and <code>false</code> otherwise
	 */
	protected static boolean isValidScheme(String scheme) {

		// if scheme is null, return true
		if ( scheme == null ) {
			return true;
		}

		if ( Character.isLetter(scheme.charAt(0)) ) {
			// check all other characters to be valid scheme chars
			for ( int i = 1; i < scheme.length(); i++ ) {
				if ( !isValidSchemeChar(scheme.charAt(i)) ) {
					return false;
				}
			}
		} else {
			return false;
		}

		return true;
	}

    /**
     * Returns whether URI starts with some special prefix. Such prefixes are
     * defined for pages in additional modules (example is news item).
     *
     * @param uri URI to check
     * @param servletContext servlet context
     * @return whether URI starts with special prefix
     */
    protected static boolean uriStartsWithSpecialPrefix(String uri,
                                                        ServletContext servletContext) {
        List prefixes = (List) servletContext.getAttribute(WebappConstants.SPECIAL_OBJECT_URI_PREFIXES_KEY);
        boolean inList = false;
        for (Iterator i = prefixes.iterator(); i.hasNext(); ) {
            String prefix = (String) i.next();
            if (uri.startsWith(prefix)) {
                inList = true;
                break;
            }
        }
        return inList;
    }

    /**
     * A comparator used to compare indexed content fields. It compares
     * them by their index value.
     */
    public static class IndexedContentFieldComparator implements Comparator {
        /**
         * Compares CFs by their index values. If first is less than second,
         * -1 is returned, if they are equal, 0 is returned, otherwise +1 is the
         * result
         *
         * @param o1 First field
         * @param o2 Second field
         * @return -1, 0 or 1
         */
        public int compare(Object o1, Object o2) {
            if ( !(o1 instanceof ContentField) || !(o2 instanceof ContentField) ) {
                throw new ClassCastException("Only ContentFields can be compared by this comparator");
            }

            ContentField cf1 = (ContentField) o1;
            ContentField cf2 = (ContentField) o2;

            String index1 = getContentFieldIndex(cf1.getIdentifier());
            String index2 = getContentFieldIndex(cf2.getIdentifier());

            if ( index1 == null || index2 == null ) {
                throw new ClassCastException("Only Indexed ContentFields can be compared by this comparator");
            }

            Double i1 = Double.valueOf(index1);
            Double i2 = Double.valueOf(index2);

            return i1.compareTo(i2);
        }
    }
}
